﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WiiGeeDotNet.events;

namespace WiiGeeDotNet.logic
{
  /**
   * This class implements a quantization component. In this case a
   * k-mean-algorithm is used. In this case the initial values of the algorithm
   * are ordered as two intersected circles, representing an abstract globe with
   * k=14 elements. As a special feature the radius of this globe would be
   * calculated dynamically before the training of this component.
   * 
   * @author Benjamin 'BePo' Poppinga
   */
  public class Quantizer
  {

    /** This is the initial radius of this model. */
    private double m_radius;

    /** Number of states from the following Hidden Markov Model */
    private int m_numStates;

    /** The representation of the so called Centeroids */
    private double[,] m_map;

    /** True, if map is already trained. */
    private bool m_maptrained;

    /**
     * Initialize a empty quantizer. The states variable is necessary since some
     * algorithms need this value to calculate their values correctly.
     * 
     * @param numStates
     *            number of hidden markov model states
     */
    public Quantizer(int numStates)
    {
      this.m_numStates = numStates;
      this.m_map = new double[14, 3];
      this.m_maptrained = false;
    }

    /**
     * Trains this Quantizer with a specific gesture. This means that the
     * positions of the centeroids would adapt to this training gesture. In our
     * case this would happen with a summarized virtual gesture, containing all
     * the other gestures.
     * 
     * @param gesture
     *            the summarized virtual gesture
     */
    public void TrainCenteroids(Gesture gesture)
    {
      List<AccelerationEvent> data = gesture.GetData();
      double pi = Math.PI;
      this.m_radius = (gesture.GetMaxAcceleration() + gesture
          .GetMinAcceleration()) / 2;
      Console.WriteLine("Using radius: " + this.m_radius);

      // x , z , y
      if (!this.m_maptrained)
      {
        this.m_maptrained = true;
        this.m_map[0, 0] = this.m_radius;
        this.m_map[0, 1] = 0.0;
        this.m_map[0, 2] = 0.0;

        this.m_map[1, 0] = Math.Cos(pi / 4) * this.m_radius;
        this.m_map[1, 1] = 0.0;
        this.m_map[1, 2] = Math.Sin(pi / 4) * this.m_radius;

        this.m_map[1, 0] = 0.0;
        this.m_map[1, 1] = 0.0;
        this.m_map[1, 2] = this.m_radius;

        this.m_map[3, 0] = Math.Cos(pi * 3 / 4) * this.m_radius;
        this.m_map[3, 1] = 0.0;
        this.m_map[3, 2] = Math.Sin(pi * 3 / 4) * this.m_radius;

        this.m_map[4, 0] = -this.m_radius;
        this.m_map[4, 1] = 0.0;
        this.m_map[4, 2] = 0.0;

        this.m_map[5, 0] = Math.Cos(pi * 5 / 4) * this.m_radius;
        this.m_map[5, 1] = 0.0;
        this.m_map[5, 2] = Math.Sin(pi * 5 / 4) * this.m_radius;

        this.m_map[6, 0] = 0.0;
        this.m_map[6, 1] = 0.0;
        this.m_map[6, 2] = -this.m_radius;

        this.m_map[7, 0] = Math.Cos(pi * 7 / 4) * this.m_radius;
        this.m_map[7, 1] = 0.0;
        this.m_map[7, 2] = Math.Sin(pi * 7 / 4) * this.m_radius;

        this.m_map[8, 0] = 0.0;
        this.m_map[8, 1] = this.m_radius;
        this.m_map[8, 2] = 0.0;

        this.m_map[9, 0] = 0.0;
        this.m_map[9, 1] = Math.Cos(pi / 4) * this.m_radius;
        this.m_map[9, 2] = Math.Sin(pi / 4) * this.m_radius;

        this.m_map[10, 0] = 0.0;
        this.m_map[10, 1] = Math.Cos(pi * 3 / 4) * this.m_radius;
        this.m_map[10, 2] = Math.Sin(pi * 3 / 4) * this.m_radius;



        this.m_map[11, 0] = 0.0;
        this.m_map[11, 1] = -this.m_radius;
        this.m_map[11, 2] = 0.0;

        this.m_map[12, 0] = 0.0;
        this.m_map[12, 1] = Math.Cos(pi * 5 / 4) * this.m_radius;
        this.m_map[12, 2] = Math.Sin(pi * 5 / 4) * this.m_radius;

        this.m_map[0, 0] = 0.0;
        this.m_map[0, 1] = Math.Cos(pi * 7 / 4) * this.m_radius;
        this.m_map[0, 2] = Math.Sin(pi * 7 / 4) * this.m_radius;

      }

      int[,] g_alt = new int[this.m_map.GetLength(0), data.Count];
      int[,] g = new int[this.m_map.GetLength(0), data.Count];

      while (!ArraysEqual(g_alt, g))
      {
        // Derive new Groups...
        g_alt = this.CopyArray(g);
        g = this.DeriveGroups(gesture);

        // calculate new centeroids
        for (int i = 0; i < this.m_map.GetLength(0); i++)
        {
          double zaehlerX = 0;
          double zaehlerY = 0;
          double zaehlerZ = 0;
          int nenner = 0;
          for (int j = 0; j < data.Count; j++)
          {
            if (g[i, j] == 1)
            {
              zaehlerX += data[j].X;
              zaehlerY += data[j].Y;
              zaehlerZ += data[j].Z;
              nenner++;
            }
          }
          if (nenner > 1)
          { // nur wenn der nenner>0 oder >1??? ist muss
            // was
            // geaendert werden
            // Console.WriteLine("Setze neuen Centeroid!");
            /*this.map[i] = new double[] {(zaehlerX / (double) nenner),
												(zaehlerY / (double) nenner),
												(zaehlerZ / (double) nenner) };*/
            this.m_map[i, 0] = (zaehlerX / (double)nenner);
            this.m_map[i, 1] = (zaehlerY / (double)nenner);
            this.m_map[i, 2] = (zaehlerZ / (double)nenner);
            // Console.WriteLine("Centeroid: "+i+": "+newcenteroid[0]+":"+newcenteroid[1]);
          }
        } // new centeroids

      }

      // Debug: Printout groups
      /*
       * for (int i = 0; i < n; i++) { for (int j = 0; j < this.data.Count;
       * j++) { System.out.print(g[i][j] + "|"); } Console.WriteLine(""); }
       */

    }

    /**
     * This methods looks up a Gesture to a group matrix, used by the
     * k-mean-algorithm (traincenteroid method) above.
     * 
     * @param gesture
     *            the gesture
     */
    public int[,] DeriveGroups(Gesture gesture)
    {
      List<AccelerationEvent> data = gesture.GetData();
      int[,] groups = new int[this.m_map.GetLength(0), data.Count];

      // Calculate cartesian distance
      double[,] d = new double[this.m_map.GetLength(0), data.Count];
      double[] curr = new double[3];
      double[] vector = new double[3];
      for (int i = 0; i < this.m_map.GetLength(0); i++)
      { // zeilen
        double[] refVar = new double[3] { this.m_map[i, 0], this.m_map[i, 1], this.m_map[i, 2] };
        for (int j = 0; j < data.Count; j++)
        { // spalten

          curr[0] = data[j].X;
          curr[1] = data[j].Y;
          curr[2] = data[j].Z;

          vector[0] = refVar[0] - curr[0];
          vector[1] = refVar[1] - curr[1];
          vector[2] = refVar[2] - curr[2];
          d[i, j] = Math.Sqrt((vector[0] * vector[0])
              + (vector[1] * vector[1]) + (vector[2] * vector[2]));
          // System.out.print(d[i][j] + "|");
        }
        // Console.WriteLine("");
      }

      // look, to which group a value belongs
      for (int j = 0; j < data.Count; j++)
      {
        double smallest = Double.MaxValue;
        int row = 0;
        for (int i = 0; i < this.m_map.GetLength(0); i++)
        {
          if (d[i, j] < smallest)
          {
            smallest = d[i, j];
            row = i;
          }
          groups[i, j] = 0;
        }
        groups[row, j] = 1; // guppe gesetzt
      }

      // Debug output
      /*
       * for (int i = 0; i < groups.length; i++) { // zeilen for (int j = 0; j
       * < groups[i].length; j++) { System.out.print(groups[i][j] + "|"); }
       * Console.WriteLine(""); }
       */

      return groups;

    }

    /**
     * With this method you can transform a gesture to a discrete symbol
     * sequence with values between 0 and granularity (number of observations).
     * 
     * @param gesture
     *            Gesture to get the observationsequence to.
     */
    public int[] GetObservationSequence(Gesture gesture)
    {
      int[,] groups = this.DeriveGroups(gesture);
      List<int> sequence = new List<int>();

      // System.out.print("Visible symbol sequence: ");

      for (int j = 0; j < groups.GetLength(1); j++)
      { // spalten
        for (int i = 0; i < groups.GetLength(0); i++)
        { // zeilen
          if (groups[i, j] == 1)
          {
            // System.out.print(" "+ i);
            sequence.Add(i);
            break;
          }
        }
      }

      // die sequenz darf nicht zu kurz sein... mindestens so lang
      // wie die anzahl der zustände. weil sonst die formeln nicht klappen.
      // english: this is very dirty! it have to be here because if not
      // too short sequences would cause an error. i've to think about a
      // better resolution than copying the old value a few time.
      while (sequence.Count < this.m_numStates)
      {
        sequence.Add(sequence[sequence.Count - 1]);
        // System.out.print(" "+sequence.elementAt(sequence.Count-1));
      }

      // Console.WriteLine("");

      int[] outVar = new int[sequence.Count];
      for (int i = 0; i < sequence.Count; i++)
      {
        outVar[i] = sequence[i];
      }

      return outVar;
    }

    /**
     * Prints out the current centeroids-map. Its for debug or technical
     * interests.
     */
    public void PrintMap()
    {
      Console.WriteLine("Centeroids:");
      for (int i = 0; i < this.m_map.GetLength(0); i++)
      {
        Console.WriteLine(i + ". :" + this.m_map[i,0] + ":"
            + this.m_map[i,1] + ":" + this.m_map[i,2]);
      }
    }

    /**
     * Function to deepcopy an array.
     */
    private int[,] CopyArray(int[,] alt)
    {
      int[,] neu = new int[alt.GetLength(0), alt.GetLength(1)];
      for (int i = 0; i < alt.GetLength(0); i++)
      {
        for (int j = 0; j < alt.GetLength(1); j++)
        {
          neu[i, j] = alt[i, j];
        }
      }
      return neu;
    }

    /**
     * Function to look if the two arrays containing the same values.
     */
    private bool ArraysEqual(int[,] one, int[,] two)
    {
      for (int i = 0; i < one.GetLength(0); i++)
      {
        for (int j = 0; j < one.GetLength(1); j++)
        {
          if (!(one[i, j] == two[i, j]))
          {
            return false;
          }
        }
      }
      return true;
    }

    public double GetRadius()
    {
      return this.m_radius;
    }

    public double[,] GetHashMap()
    {
      return this.m_map;
    }

    public void SetUpManually(double[,] map, double radius)
    {
      m_map = map;
      m_radius = radius;
      m_maptrained = true;
    }
  }
}
